"""
nlu技术细节：
系统意图：goodbye（会话终止），greet（会话开始），affirm（肯定），deny（否定），stop（停止），
闲聊意图：chitchat/whoyouare（询问你是谁），chitchat/whatyoucando（询问你能做什么），或者其他意图，可以直接接入LLM;
业务意图：agent/refresh （刷新表格），agent/openwindow  （打开窗口）

synonym是存储同义词信息的，为了进一步降低系统的复杂度，只用于系统意图，业务意图中同义词的扩充，对于闲聊意图不做任何约束；
lookup是存储查找表的，比如刷新组件，这个组件可以用一个查找表来进行存储，那么可以进一步的提高这些组件的准确性；
regex是存储正则表达式的，在业务系统中比如填入网址信息是非常高频的一个动作，那么可以存储正则来识别网址；

目前在NLU模块主要是使用Bert模型来做意图的分类，这块需要研究怎么让LLM来实现意图的分类，同时将对应的
实体信息提取出来。即使判断错误后对应的动作无法完成时，可以将意图转移给llm
"""


"""
rasa core

domain(领域)：定义对话机器人需要知道的所有信息，包括意图（intent），实体（entity），词槽（slot）、
动作（action）、表单（form）和回复（response）.
其中意图和实体表示输入的范围；词槽和表单相当于内部的变量，用来表征状态和存储记忆；动作给定了模型输出的范围；
回复字段作为对话机器人回复的模版。

意图和实体都是列表，用于高速机器人的意图和实体识别。

动作是对话管理模型的输出，用于指导对话机器人的行为。比如调用外部API，或者更新词槽的值。
在rasa中以utter_开头的动作表示渲染同名的模版给到用户，这是约定；

词槽定义了机器人在对话过程中需要跟踪的状态信息。

回复定义了机器人回复的模版。格式都是以utter_开头的。

会话是指用户和机器人之间的对话过程。一个会话可能横跨很多轮对话，主要支持session_expiration_time和carry_over_slots_to_new_session
前者表示在用户的最新消息多久后，会话会被认为过期；后者表示当新的会话开始时，是否应该将上一个会话的词槽延续（继承）到新的会话。

全局性配置store_entities_as_slots表示是否将实体信息存储为词槽，默认为true

故事（story）用于学习对话管理知识，不仅需要记录用户的语义表达，还需要记录系统内部正确的状态变化

每个story都是stories列表中的一个元素，故事本身的结构是字典，必须要有story和steps，
story键给出的值代表这个故事的备注，提供故事的信息；
故事的主体在steps键的内容中，列表的线性关系表示用户和机器人之间的交互过程。

用户消息存储用户的意图和实体信息

机器人动作名在YAML中使用action:action_ask_howcanhelp表示，这里action_ask_howcanhelp就
是机器人动作名，对于复杂的故事，可能存在用户请求一次后rasa连续执行多次动作的情况。

动作返回事件：对于内置动作，Rasa可以在后续处理中按照动作的类型自动给出返回的事件。
但是当使用自定义事件的故事时，由于Rasa无法在训练阶段确定这样的自定义动作会给对话
状态带来什么样的改变，因此需要开发者手动给出动作改变的状态。这种状态的改变称为事件。
常用的事件包括词槽事件和active_loop事件。
词槽事件：能对词槽状态进行改变的事件，定义好事件修改词槽；
active_loop事件：主要负责激活和取消激活表单的事件。

辅助符号：
- 检查点符号：用来减少故事中的重复部分，名字相同的检查点可以相互跳转；但是不能滥用，否则会导致故事逻辑混乱；
- or语句：用来表示多种可能的情况，可以精简故事；

动作：接受用户输入和对话状态信息，按照业务逻辑进行处理，并输出改变对话状态的事件和回复用户的消息；
回复动作和领域里面的response关联在一起，当调用这一类动作时，会自动查找回复中同名的模版并进行渲染。
所以这类动作和回复模版一样使用utter_开头。

表单：任务型对话的一个重要模式就是多次和用户交互，用来收集任务所需要的要素，直到所需要的要素收集完整。
这种模式常被称为填表，填表的模式十分重要。

默认动作：rasa对比较常见的和业务无关的动作提供了默认的动作。
action_listen:停止预测动作，等待用户输入；
action_restart:重启对话,清理对话历史和词槽；
默认的动作是可以被同名的自定义动作替代的；

自定义动作：多数对话任务都需要开发者自行定义动作，自定义动作完全由开发者实现，
可以满足各种后端交互和计算的需求，常见的后端交互包括查询数据库或者发情对第三方API的请求。


词槽事机器人的记忆机制，词槽是以键值对的形式存在的，用于记录对话过程中发生了
那些关键信息，这些信用可能来源于用户输入或者后端。这些插槽信息对于对话
的走向有着至关重要的作用，或者说这些信息会被对话管理系统用于预测下一个动作；

在词槽中，influnce_conversation来设置该词槽对对话的过程是否产生影响；
词槽的类型比较常用的是text，bool和category，当然还有float，list和any；

词槽的映射：映射制定了在对话过程中如何自动地为这个词槽对应的只，一个词槽同时有
多个映射在运行时会按照从上到下的顺序依次执行；

策略负责学习故事，从而预测动作，策略需要通过特征提取组件将故事转化为对话状态，
进一步得到对话状态特征，按照对话特征预测下一个对话动作。

在config.yaml文件中，policies键使用保留给策略配置的，类似于NLU部分的流水线
配置，策略由多个列表构成，每个列表都是一个字典，字典有name和配置想，name是
组件的名字，其他值则是配置选项。

内置的策略：TEDPolicy、MemoizationPolicy
也可以给每个策略定义优先级，优先级数值越高，策略有限；

另外rasa提供了数据增强的功能来丰富故事多样性；

端点，endpoints.yml定义了Rasa Core和其他服务进行连接的配置信息，及端点；

Rasa SDK和自定义动作，在生产环节只安装rasa-sdk就可以了。

自定义动作：需要继承SDK的动作类，这样服务器就能自动发现并注册动作。
重写name()方法返回一个字符串，可以想服务器申请这个动作的名称，重写run()方法，
开发者可以获得当前的对话信息（tracker对象和领域对象）和用户消息对象（dispatcher）
开发者可以利用这些消息来完成业务动作。如果想要对当前的对话状态修改（比如修改词槽），
则需要通过返回事件（event）(可以是多个事件)的形式发送到rasa服务器，即使对话状态没有
发生任何变化，也需要返回一个空的列表。

tracker对象，对话状态追踪，就是对话的历史记忆，开发者主要是获取到对应的对话状态，即业务输入；

事件对象：在自定义动作中，如果需要更改对话状态，则需要用到事件对象。










"""







"""
Response Selector 实现FAQ和闲聊功能

如何定义用户问题：ResponseSelector需要采用group/intent的格式明明，其余格式一样，
Rasa把group/intent格式中的group部分称为检索意图（retrieval intent）。

如何定义问题的答案：ResponseSelector在response字段中存放答案，
约定：intent的问题都需要有一个名为utter_intent的response作为答案；
responses:
  utter_chitchat/ask_name:
    - text: "我的名字是Silly，一个Rasa文档机器人。"
这样我们已经将问题和答案建立了关联。

如何训练Rasa：为了让Rasa具有良好的泛化能力回答问题，需要让ResponseSelector在
现有的数据上进行训练，在pipeline中添加ResponseSelector组件即可。

"""


"""
基于规则的对话管理

fallback:经常会出现对话机器人无法处理的情况，用户说话不清楚或者超出机器人
能提供的服务范围，这个时候需要一个兜底的fallback操作，确保即使出错也可以
提供一个合理的回复。

NLU fallack：主要负责处理在NLU阶段理解用户意图困难或者模糊的情况，这里
我们可以使用FallbackClssfier组件。
pipeline:
  - name: FallbackClassifier
    threshold: 0.3
    ambiguity_threshold: 0.1
如果在所有意图分类组件预测的结果中，最高的置信度不大于或者等于0.3，或者
最高的前两个意图得分差不超过0.1分，那么NLU的意图就被毁替换成nlu_feedback.
可以通过rule，将nlu_feedback映射成为我们想要的动作，比如让用户重新澄清或者选择；
也可以转向到action_two_stage_fallback.

策略fallback：Rasa在预测下一个动作时，如果预测结果置信度不高，就需要fallback；
如果得分最高的两个动作得分差异过小，也需要fallback。
在Policies上可以进一步的进行配置：
policies:
  - name: RulePolicy
    core_fallback_threshold: 0.3
    core_fallback_action_name: action_default_fallback
    enable_fallback_prediction:True
    
默认的action_default_fallback是一个内置的动作，会转移给utter_default的模版，
并返回给用户，开发者可以改变选项core_fallback_action_name的设置

意图触发动作

内建意图触发动作
Rasa允许开发者使用intent{"entity":val}的快捷方式来表达意图和实体信息，
一个用途是测试机器人，另一个用途就是作为用户单击按钮时返回给Rasa的有效载荷，
这里不展开。

自定义意图触发动作
开发人员想要保证当某个意图出现时，无论什么样的上下文都能百分百触发某个或者
多个特定的动作时，就需要使用RulePolicy的功能，在启用状态下，
用户表达了some_intent意图后，RulePolicy能够确保百分百地触发some_action动作；

表单：以完成任务为核心目的的对话过程，可以理解为引导用户填写表单的过程；
- 机器人会问用户想干什么；
- 用户表达了自己的需求（意图和实体）；
- 机器人会按照用户的意图，确定合适的表单，并将用户在对话中提供的实体信息填入表中；然后
机器人会查看表单中缺失的字段，按照一定的策略（字段顺序）访问用户关于缺失字段的问题；
- 用户提供了缺失字段的信息；
- 机器人将缺失信息填入表单，询问下一个缺失字段；
- 如此反复迭代，直到某一时刻，机器人发现表单已经填写完整，于是开始执行具体的任务；
为了使Rasa正确地基于表单的对话管理，开发者需要将RulePolicy加入配置文件。

定义表单：使用表单之前，需要定义一个表单，这里的表单的名字是front_code_form
forms:
 front_code_form:
  required_slots:
    element:
      - type: from_entity
        entity: text
        intent: [inform, request_element]
    event:
      - type: from_entity
        entity: text
        intent: [inform, request_event]
    component:
      - type: from_entity
        entity: text
        intent: [inform, request_component]
    action:
      - type: from_entity
        entity: text
        intent: [inform, request_action]
        
每一个表单都需要制定必须的词槽，比如element,event,action等；

激活表单：开发者需要设定什么时候进入该表单，通过rule制定
rules:
  - rule: 激活前端代码form
    steps:
      - intent: 前端代码生成
      - action: front_code_form
      - active_loop: front_code_form
      
如果用户的意图是代码生成，就会执行front_code_form这个动作，这个动作
的副作用（在对话状态中引起的变化）是进入名为front_code_form的active_loop，
和前面表单定义的front_code_form重名，因此会进入对应的填槽-询问的循环过程。

执行表单任务：当表单条件都满足后（也就是所请求的词槽全部已经获得时），就可以执行
表单任务了。执行什么动作是通过规则来设定的。
- rule: 提交表单
 condition:
  - active_loop: front_code_form
 steps:
  - action: action_front_code_form
  - active_loop: null
  - slow_was_set:
   - requested_slots: null
  - action: action_code_form_submit
  
这个规则定义了front_code_form的active_loop结束，且所有请求的词槽都已经完成，
也就是requested_slots为null时，执行动作action_code_form_submit，开发者
所有的业务逻辑都在这里面完成，比如调用第三方API查询天气。




"""


"""
基于知识库的问答
常见的对话机器人领域的难题是用户不仅会用名字来指代他们感兴趣的对象，还会用这个，
那个，第一个，第二个指代词。对话管理系统在处理这种表达方式需要记住之前发送给用户
的信息（如歌曲列表），才能正确地将这些指代词解析成正确的对象。
开发者需要创建一个继承ActionQueryKnowledgeBase的类的自定义动作，可以
处理对象和属性的查询。

ActionQueryKnowledgeBase
创建知识库：用于问答用户请求的数据都存储在知识库中，知识库可以用于存储很多复杂结构
的数据。当然也可以使用Neo4j，现在我们使用InMemoryActionQueryKnowledgeBase,
在本地通过Json来创建一个知识库数据，知识库应该为每个对象设置键和值，
{"":[]}
每一个例子都至少有一个name属性，name是唯一的，使用数据文件开发者就可以创建一个实例，
这个实例将被传递给动作文件用于后续的查询。

NLU数据
让机器人知道用户想要进行知识库信息查询，开发者需要定义一个意图来表示这样的意图。比如
query_knowledge_base, 而ActionQueryKnowledgeBase可以处理的请求分为两类。
- 用户想要获得某个对象类型的列表，无论附加或者不附加过滤条件；
- 用户想要获得对象的某个属性；
无论是哪种请求，都需要划分为query_knowledge_base意图
nlu：
 - intent: query_knowledge_base
   examples: |
    - 有什么好听的[歌曲](object_type)
    - 给我列出一些[男](gender)[歌手](object_type)
    - 给我列出一些[周杰伦](singer)的[歌曲](object_type)
    - [刚才那首](mention)属于什么[专辑](attribute)
这个例子中出现了三种实体，这三个实体对于完成知识库查询非常重要。
object_type：表示用户想要查询的对象类型，比如歌曲，歌手，专辑等；
attribute：知识库中对象的所有属性都应该在NLU的训练数据中被标记为attribute实体。
mention：表示用户提到的对象，比如刚才那首歌曲，周杰伦等；
当然这些实体需要在domain中定义。

自定义基于知识库的动作
通过继承ActionQueryKnowledgeBase并将知识库实力作为参数传递给构造函数，开发者
可以创建自己的基于知识库的动作。

工作原理：
ActionQueryKnowledgeBase即需要当前提取的实体信息，又需要先前对话留下的词槽，
这样才能明确查询什么。

对象查询：为了能够在知识库中查询对象，用户请求中应该包含对象的类型。
比如：有什么好听的歌曲？
这个问题就包含了用户需要查询的对象：歌曲，在NLU部分，这个问题会被解析为
“有什么好听的[歌曲](object_type)？”随后歌曲被映射为song，这样机器人
就会去查询song知识库，从而列出里面的实体。

属性查询：如果想获取某个对象特定的信息，那么用户应该在请求中包含对象
信息和感兴趣的属性。比如[七里香](song)属于什么[专辑](attribute)？,
这个问题中，用户需要查询的对象是七里香，感兴趣的属性是专辑。

解析指代：用户在对话中可能会使用指代，比如刚才那首，周杰伦等。
动作的智星需要具备将这些指代词（如第一个）正确地映射成知识库的对象的
能力。细化来说，ActionQueryKnowledgeBase可以解析2种不同的指代。
- 序数指代，如第一个；
- 代词指代，如这家；
序数指代用一个对象在列表中的位置来指代该对象的做法为序数指代，比如第一个，
第二个，最后一个，任何一个等。用户通常在上一轮对话中机器人给出了
列表结果的时候使用序数指代。ActionQueryKnowledgeBase有自己默认的
映射关系。但是序数映射字典不包含第一个这样的键，开发者需要使用实体值
映射的方法将用户表达的不规则的序数指代映射成标准的序数指代，如把第一个
映射成1，把最后一个映射成LAST等。可以使用NLU的同义词映射synonym;

其他指代：比如他是哪一年发行的时候，它是指代上一轮对话中的实体，
当NLU检测到mention的值是它的时候，ActionQueryKnowledgeBase就会
将这个指代词映射成对话中最后提及的对象；

自定义：ActionQueryKnowledgeBase返回给用户的消息默认是英文版；
utter_objects():当用户请求机器人返回对象列表时，调用这个方法，
可以将对象列表的情况回复给用户，我们可以重写这个方法来实现自己的需求；
utter_attribute_value():当用户请求机器人返回对象的某个属性值时，
调用这个方法，我们可以重写这个方法来实现自己的需求；
utter_ask_attribute():当用户请求机器人返回对象的某个属性值时，
调用这个方法，我们可以重写这个方法来实现自己的需求；
InMemoryKnowledgeBase：用于存储知识库数据的类。也可以重写来实现自定义需求；
get_key_attribute_of_object():追踪用户最后提及的对象，需要存储一个对象
的关键属性。每一个对象都应该有一个全局唯一的关键属性，就像关系型
数据库中的主键。开发者可以调用set_key_attribute_of_object()来设置；
get_representaion_function_of_object(),当用户要求机器人输出
所有的餐厅是，并不需要列出餐厅的所有明细。开发者应该提供一个简单、有意义
并且唯一的标志，在多数情况下，一个对象的name字段就是标志，也可以调用
set_representation_function_of_object()来更改设置；
set_ordinal_mention_mapping();序数指代的映射用于将一个序数指代词，
如第二个转化成列表中的一个对象，开发者可以仿照默认设置实现，通过调用
set_ordinal_mention_mapping来更改设置，从而实现自定义。

创建自定义知识库
在数据特别多或者结构非常复杂的情况下，需要创建自定义知识库，先继承
KnowledgeBase，再实现get_objects(),get_object()和get_attribute_of_objects()
方法。

"""


"""
实体角色和分组
在某些情况下，只知道实体的类型和值是无法完成任务的，需要在更细的力度上区分
实体的类型。

实体角色
Rasa提供了实体角色（entities role）这一特性，用来却分相同实体的不同
角色。比如盯机票，在没有区分出发地和目的地的情况下，后端无法区分从上海
到北京的机票和从北京到上海的机票。在后端看来，只知道有两个类型为城市的
实体但是不清楚每个实体的语义角色（出发地还是目的地），在训练和预测时
需要使用以下格式：从[北京]{"entity":"city","role":"departure"}
到[上海]{"entity":"city","role":"destination"}的机票以及
从[上海]{"entity":"city","role":"departure"}
到[北京]{"entity":"city","role":"destination"}的机票，
通过角色信息将同样为city类型的北京和上海划分为目的地和出发地两种
角色。这些角色信息可以在表单的词槽映射中使用。

实体分组
有些情况会出现多组实体，每组实体描述了一个子任务，这个时候就需要细分实体，
从而将实体按照语义分组。比如订餐时，用户可能会定两份煎饼果子，一份是
大份的煎饼果子加香菜，另一份是小份的煎饼果子加辣椒。由于用户的表达
很灵活，如果无法将实体分组，那么后端无法确定两份煎饼果子的配置究竟
怎么样。Rasa提供了实体分组（entities group）这一特性来解决这一问题；
“买煎饼果子，一份[大]{"entity":"size","group":"1"}的加[香菜]
{"entity":"garnish","group":"1"}，一份[小]{"entity":"size","group":"2"}
的加[辣椒]{"entity":"garnish","group":"2"}”
在使用过程中，可以读取实体的分组情况，因此动作可以正确地处理多组请求一起
的情况。
当然实体分组和角色目前Rasa只有DIETClaasifier和CRFEntityExtracor
支持该特性。
"""


"""
测试和生产环节部署
如何测试机器人的表现：

对NLU和故事数据进行校验
使用Rasa自带的工具对领域、NLU训练数据和故事
进行验证，从而发现其中可能的错误和不一致的情况，在训练前可以对数据
进行校验来提前发现这些错误，避免训练之后才报错从而提高开发效率。
rasa data validate
建议在更改数据和配置后都执行这样的检查，可以最大程度地提前发现潜在问题；

编写测试用的故事
Rasa提供了一个端到端的测试框架，开发者使用测试故事格式（由原油的故事格式
扩展而来）可以将用户的输入和期望的结果编写成测试用例，从而让Rasa测试框架
自动进行测试；
测试故事和普通故事的重要区别是测试故事包含输入文本（可以是文本列表）。
测试故事NLU和对话模型进行端到端测试，因此必须包含对NLU模型评估必不可少
的输入文本
测试故事和普通故事有两个区别：不同的实体表示格式和仅存于测试故事的输入文本。
在测试故事中，实体列表和输入文本进行整合（如帮我找[中国]{"entity":"cuisine"}餐馆）。
这样可以准备表示实体，方便进行NLU模型的评估。

评估NLU模型
虽然测试故事能够在测试整个对话过程中覆盖NLU模型的测试，但是编写成本
非常高，因此无法拥有大量测试用例和较大的测试覆盖面。针对NLU模型的测试，
可以使用Rasa自带的NLU测试功能。通常，测试用的NLU数据和训练用的NLU数据
来自同一批数据，经过分割而成。在Rasa中可以使用以下明亮对数据集进行分割。
rasa data split nlu
这个命令会读取data目录下的nlu数据（用--nlu参数可以修改），使用80%
（可以使用--training-fraction更改比例）的数据作为训练数据，其余数据作为
测试数据。新生成的训练数据和测试数据会保存在train_test_split目录中，
（可以使用--out参数修改）。
rasa test nlu --nlu train_test_split/test_data.yml
在执行完成后，开发者可以在result目录下找到所有测试结果文件。
不同的NLU管道可以有不同的结果文件，比如intent_errors.json和
intent_report.json，前者会报告测试数据中所有失败的NLU样本，
后者会展示模型评估情况，即包含关于某个意图的指标，又包含综合性的指标。

评估对话管理模型
Rasa评估对话管理模型的性能很容易，使用
rasa test core --stories test_stories.yml --out results
test_stories.yml是包含了测试故事的文件。

在生产环节部署机器人
部署时间
最小可行性产品策略。该策略强调用最快，最简明的方式建立一个满足关键
需求的可用的产品原型，随后通过迭代来完善细节。
在Rasa中，当产品能够处理最重要的（而不是全部）的时候就可以将产品作为
MVP进行发布。Rasa推荐在产品研发的早起使用Rasa X来给种子用户测试，不断
改善模型，知道能够达到MVP标准，此时就可以开始部署生产环境。

选择模型存储方式
单机我们将模型存储在本地的硬盘，大规模的部署是，训练过程和服务过程是
分离的，训练好的模型会放到中心化的存储系统中，服务会按需将模型拉到本地并
自动化部署。

HTTP模型存储
在endpoint.yml中
models:
 url:http://oss
 wait_time_between_pulls:100
在默认情况下，Rasa服务器会每次间隔100s尝试从HTTP服务器获取新的模型
文件(zip格式的压缩文件)，也可以设置为None，只获取一次。为了能够在
不下载模型的前提下判断模型是否更新，Rasa会根据HTTP服务器的ETag信息来确定。

基于云的模型存储
Rasa支持从Amazon，GCS，Azure中拉取模型；


选择tracer store
Rasa中所有对话过程都要被tracker对象存储起来，实际大规模工业生产环境中，
需要大量使用负责均衡和动态扩容等技术。前后两条同一个用户的消息，可能会
发送不同的服务器处理。这个时候就需要将用户的对话历史存储在所有服务器
都能访问到的地方，当用户请求达到服务器是，通过存储服务将该用户的对话
过程下载回来。当用户请求结束时自动将对话过程存储在存储服务中，当下一次
用户到达服务器时自动将用户对话过程还原到系统中。这样的组件在Rasa体系
中称为tracker store。
InMemoryTrackerStore
默认存储，直接使用内存作为存储介质，重启服务器将会导致数据完全丢失，
组件不能在多个服务器间共享，这个是单机存储方案；
SQLTrackerStore
SQL数据库存储；
RedisTrackerStore
使用Redis存储
MongoTrackerStore
使用mongodb来存储
DynamoTrackerStore
使用dynamoDb来存储
自定义tracker store
开发者可以通过继承tracker store的方式来完成新存储方案的实现；

选择lock store
在实际大规模工业生产环境中，会使用负载均衡和动态扩容的技术，用户消息
可能会发送到不同的服务器处理，因此可能会存在消息顺序错乱的问题。
为了解决这一问题，Rasa引入了lock store: 一种分布式锁，lock
store通过锁定处理顺序，确保消息按照正确的处理顺序。
InMemoryLockStore
在单个晋城市会发挥作用，在同时运行多个Rasa服务器，单机和多机情况下，该lock
store没有任何的效果。
RedisLockStore
使用Redis存储，能够处理多机多进程的各种情况；

单机高并发设置
在默认情况下，Rasa服务器只使用一个Worker。只有将环境变量
SANIC_WORKER的值设置为1以上，同时当locker store的设置不是
InMemoryLockStore时才可以启用多个worker。
在默认情况下，Rasa动作服务器也值使用一个worker。将环境变量ACTION
_SERVER_SERVER_SANIC_WORKERS的值设置为1以上就可以启动多个worker。

性能测试
为了测试性能，我们需要连接Rasa进行处理，连接RASA的简单方法是利用RESTFul接口
通过配置credentials.yml，我们开启REST接口。
通过向Rasa服务器的/webhooks/rest/webhook地址发送Pots请求，可以
向Rasa应用发送请求。
在知道如何发送用户请求，我们就可以使用Jmeter等测试工具对Rasa应用的吞吐量
和响应时间进行测量。在配置高性能Rasa服务器可以在减少一半响应时间的基础上


"""































